// 实现环形队列作为 : 生产者和消费者的中间交易场所

// 所以 , 环形队列最终让生产者生产数据 , 消费者消费数据
// 队列为空 , 代表什么 ?? 代表没有数据了 , 所以生产者要生产了
// 队列为满 , 代表什么 ?? 代表数据满了   , 所以消费者要消费了

// 具体怎么生产 , 怎么消费 ?? 请看博客图
// 就一句话 : 无论是生产者还是消费者 , 必须先申请信号量才能访问临界资源

#pragma once

#include <iostream>
#include <vector>
#include "Sem.hpp"
#include "Mutex.hpp"

namespace CircularQueueModel
{
    // 1. 对信号量组件命名空间展开
    using namespace SemModule;
    // 2. 对锁组件命名空间展开
    using namespace MutexModel;

    // 这里设定环形队列的大小
    const int default_queue_size = 5;

    // 实现模版
    template <typename T>
    class CQueue
    {
    public:
        CQueue(int cap = default_queue_size)
            : _cap(cap)
              ,_cqueue(cap)
              ,_blank_sem(cap) // 开始位置全有
              , _pstep(0)
              , _data_sem(0) // 开始数据是0
              , _cstep(0)
        {
        }

        // *********** 考虑一个问题 ***********
        // 既然要加锁 , 那是先申请锁呢 ?? 还是先申请信号量呢 ??
        // 推荐先申请信号量 , 因为信号量是对资源的预定机制,资源谁访问 , 线程啊 !
        // 所以如果我都预定资源都没有 , 我还去竞争锁 , 有意义吗 ?? 没有 , 因为我肯定不能访问资源了
        // 所以 , 先申请信号量 , 这里效率更高 !!!!!!!!
        // ***********************************

        // 生产者生产数据 -- 入队列
        void Equeue(const T &in)
        {

            // 生产者生产一个数据代表多了一个数据
            // 1 .先要申请信号量 , 怎么申请 ?? 对于生产者来说 , 插入一个数据就少一个位置
            // 所以 , 要P操作

            // 1. 申请信号量
            _blank_sem.P();
            // 加锁
            {
                LockGuard lockguard(_p_mutex);
                // 2. 生产
                _cqueue[_pstep] = in;
                // 3. 更新下标
                _pstep++;
                // 4. 维持环状结构
                _pstep %= _cap;
            }

            // 要唤醒 , 消费者 , V操作 , 怎么唤醒 ?? 因为多了一个数据 , 所以++表示数据的信号量
            // 所以要用消费者信号量
            // 5. 唤醒
            _data_sem.V();
        }

        // 消费者消费数据 -- 出队列
        T Pop()
        {
            // 消费者消费数据 , 少一个数据, P操作
            T data;
            // 加锁
            {
                LockGuard lockguard(_c_mutex);
                // 1. 申请信号量
                _data_sem.P();
                // 2. 消费
                data = _cqueue[_cstep];
                // 3. 更新下标
                _cstep++;
                // 4. 维持环状结构
                _cstep %= _cap;
            }
            // 5. 唤醒生产者
            // 消费一个数据就多一个空位置 ,所以要用空位置信号量的 V操作
            _blank_sem.V();

            return data;
        }

        ~CQueue()
        {
        }

    private:
        // 环形队列的特点就是 : 队列的大小是固定的 , 所以这里我们用数组模拟
        std::vector<T> _cqueue;
        // 环形队列 , 所以单纯数组的话最终访问一定会越界 , 所以我们使用取模控制
        // 取模模什么 ?? 模队列总大小 , 这样即使越界了 , 模后就回到开始位置
        int _cap; // 环形队列的总容量

        // 要存在生产者和消费者的下标位置,因为要插入消费数据要知道位置,因为是数组
        int _pstep; // 生产者
        int _cstep; // 消费者

        // 需要两个信号量 , 分别表示 , 一个表示生产者位置 , 一个表示消费者数据
        Sem _blank_sem; // 表示空位置的信号量
        Sem _data_sem;  // 表示数据的信号量

        // 加锁
        // 考虑需要几把锁 ????
        // 如果多线程情况下 , 多个线程可以同时访问临界资源吗 ?? 可以的
        // 环形队列 , 在生产的时候 , 我能继续消费吗 ?? 能 , 不影响 , 因为天然就两个指针在走
        // 两个指针可以同时走啊 !!!!!!!!!

        // 所以 , 消费者很多同时消费不同数据 , 需要为消费者提供一把独立锁
        // 生成者很多 , 同时生成很多数据 , 需要为生产者题库一把独立锁
        // 所以 , 环形队列天然就支持锁分离

        // 维持多生产和多消费
        Mutex _p_mutex; // 生产者互斥
        Mutex _c_mutex; // 消费者互斥
    };
}